package it.unibo.refolding.pico;

import it.unibo.refolding.common.MyRuntimeException;
import it.unibo.refolding.common.Sleeper;

import java.awt.AWTException;
import java.awt.Color;
import java.awt.Point;
import java.awt.Robot;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.util.Locale;

import com.google.common.base.Preconditions;

public class RobotNanoDriver extends AbstractNanoDriver {

	private final Robot robot;
	
	private final Point rampSizeLocation, scanRateLocation, numberOfSamplesLocation, zScanStartLocation, reverseVelocityLocation, xOffsetLocation, continuousLocation, stopLocation, surfaceDelayLocation, singleLocation, realtimeLocation, captureContinuousLocation;
	private final Point startRampColumn1Location;
	private final Point endRampColumn1Location;
	private final Point delayColumn1Location;
	private final Point velocityColumn1Location;
	private final Point deltaColumn2Location;
	private final Point velocityColumn2Location;
	private final Point velocityColumn3Location;
	private final Point deltaColumn3Location;
	private final Point loopCountColumn3Location;
	private final Point runScriptLocation;
	private final Point captureButtonLocation;
	private final Point abortCaptureLocation;
	private final Point startStripChartLocation;
	private final Point quitStripChartLocation;
	private final Point resetStripChartLocation;
	private final Point forceModeLocation;
	private final Point scriptModeLocation;
	
	public RobotNanoDriver(NanoDriverConfig config) {
	  super(25e-3);
		try {
			robot = new Robot();
		} catch (AWTException e) {
			throw new RuntimeException("Cannot start robot: " + e, e);
		}
		this.rampSizeLocation = config.rampSizeLocation;
		this.scanRateLocation = config.scanRateLocation;
		this.numberOfSamplesLocation = config.numberOfSamplesLocation;
		this.zScanStartLocation = config.zScanStartLocation;
		this.reverseVelocityLocation = config.reverseVelocityLocation;
		this.xOffsetLocation = config.xOffsetLocation;
		this.continuousLocation = config.continuousLocation;
		this.stopLocation = config.stopLocation;
		this.surfaceDelayLocation = config.surfaceDelayLocation;
		this.singleLocation = config.singleLocation;
		this.realtimeLocation = config.realtimeLocation;
		this.captureContinuousLocation = config.captureContinuousLocation;

		this.startRampColumn1Location = config.startRampColumn1Location;
		this.endRampColumn1Location = config.endRampColumn1Location;
		this.delayColumn1Location = config.delayColumn1Location;
		this.velocityColumn1Location = config.velocityColumn1Location;
		this.deltaColumn2Location = config.deltaColumn2Location;
		this.velocityColumn2Location = config.velocityColumn2Location;
		this.deltaColumn3Location = config.deltaColumn3Location;
		this.velocityColumn3Location = config.velocityColumn3Location;
		this.loopCountColumn3Location = config.loopCountColumn3Location;
		this.runScriptLocation = config.runScriptLocation;
		this.captureButtonLocation = config.captureButtonLocation;
		this.abortCaptureLocation = config.abortCaptureLocation;
		this.startStripChartLocation = config.startStripChartLocation;
		this.quitStripChartLocation = config.quitStripChartLocation;
		this.resetStripChartLocation = config.resetStripChartLocation;
		this.forceModeLocation = config.forceModeLocation;
		this.scriptModeLocation = config.scriptModeLocation;
	}

	@Override
	public void zScanStartM(double value) {
		Preconditions.checkArgument(!Double.isNaN(value), value);
		// Precision of 1 femtometre.
		moveClickTypeAndEnter(zScanStartLocation, "ZScanStart", String.format(Locale.ROOT,
				"%.6f nm", value * 1e9));
		robot.waitForIdle();
	}
	
	@Override
	public void reverseVelocityNmOrUmPerS(double value) {
		Preconditions.checkArgument(!Double.isNaN(value), value);
		moveClickTypeAndEnter(reverseVelocityLocation, "Reverse Velocity", format3f(value));
		robot.waitForIdle();
	}

  static String format3f(double value) {
    return String.format(Locale.ROOT, "%.3f", value);
  }

  /**
   * %.4f is needed so as to make the approachRampSize equal to 0.0001 nm
   * for refolding, which is the minimum ramp size so that the column is
   * not completely ignored.
   */
  static String format4f(double value) {
    return String.format(Locale.ROOT, "%.4f", value);
  }
	
	@Override
	public void xOffsetM(double value) {
		Preconditions.checkArgument(!Double.isNaN(value), value);
		moveClickTypeAndEnter(xOffsetLocation, "X offset", String.format(Locale.ROOT,
				"%.3f nm", value * 1e9));
		robot.waitForIdle();
	}

	private void moveClickTypeAndEnter(Point p, String label, String s) {
		robot.mouseMove(p.x, p.y);
		clickTypeAndEnter(s);
	}

	private void clickTypeAndEnter(String s) {
		click();
		typeAndEnter(s);
	}

	private void typeAndEnter(String s) {
		for (char c : s.toCharArray()) {
			switch (c) {
			case 'n':
				pressAndRelease(KeyEvent.VK_N);
				break;
			case 's':
				pressAndRelease(KeyEvent.VK_S);
				break;
			case 'm':
				pressAndRelease(KeyEvent.VK_M);
				break;
			case ' ':
				pressAndRelease(KeyEvent.VK_SPACE);
				break;
			case '.':
				pressAndRelease(KeyEvent.VK_PERIOD);
				break;
			case '-':
				pressAndRelease(KeyEvent.VK_MINUS);
				break;
			default:
				assert '0' <= c & c <= '9';
				// VK_0 thru VK_9 are the same as ASCII '0' thru '9'.
				pressAndRelease((int) c);
				break;
			}
		}
		pressAndRelease(KeyEvent.VK_ENTER);
	}

	private void click() {
		robot.mousePress(InputEvent.BUTTON1_MASK);
		robot.mouseRelease(InputEvent.BUTTON1_MASK);
	}

	private void pressAndRelease(int key) {
		robot.keyPress(key);
		robot.keyRelease(key);
	}

	@Override
	public void rampSizeM(double rampSizeM) {
		moveClickTypeAndEnter(rampSizeLocation, "RampSize", String.format(Locale.ROOT, "%.6f nm",
				rampSizeM * 1e9));
		robot.waitForIdle();
	}

	@Override
	public void scanRate(double scanRateHz) {
		moveClickTypeAndEnter(scanRateLocation, "ScanRate", String.format(Locale.ROOT, "%.5f",
				scanRateHz));
		robot.waitForIdle();
	}

	@Override
	public void numberOfSamples(int numberOfSamples) {
		moveClickTypeAndEnter(numberOfSamplesLocation, "NumberOfSamples", Integer.toString(numberOfSamples));
		robot.waitForIdle();
	}

	@Override
	public void single() {
		pressButton(singleLocation, "Single");
		robot.waitForIdle();
	}

	@Override
	public void continuous() {
		pressButton(continuousLocation, "Continuous");
		robot.waitForIdle();
	}

	@Override
	public void stop() {
		pressButton(stopLocation, "Stop");
		robot.waitForIdle();
	}

	private void pressButton(Point p, String label) {
		robot.mouseMove(p.x, p.y);
		click();
	}

	@Override
	public void surfaceDelay(double s) {
		String v = String.format(Locale.ROOT, "%.3f ms", s * 1e3);
		moveClickTypeAndEnter(surfaceDelayLocation, "SurfaceDelay", v);
		robot.waitForIdle();
	}

	@Override
	public void runScript() {
		pressButton(runScriptLocation, "Run");
		robot.waitForIdle();
	}

	@Override
	public void delayColumn1(double s) {
		Preconditions.checkArgument(s >= 0, s);
		moveClickTypeAndEnter(delayColumn1Location, "Delay Column 1", "0.0001");
		final String ss = format3f(s);
		typeAndEnter(ss);
		robot.waitForIdle();
	}

	@Override
	public void velocityColumn1(double velocityMPerS) {
		velocityColumn(velocityMPerS, velocityColumn1Location, 1);
	}
	
	@Override
	public void velocityColumn2(double velocityMPerS) {
		velocityColumn(velocityMPerS, velocityColumn2Location, 2);
	}
	
	@Override
	public void velocityColumn3(double velocityMPerS) {
		velocityColumn(velocityMPerS, velocityColumn3Location, 3);
	}
	
	private void velocityColumn(double velocityMPerS, Point location, int column) {
		String name = "Velocity Column " + column;
		moveClickTypeAndEnter(location, name, "0.0001");
		final String s = String.format(Locale.ROOT, "%.3f", velocityMPerS * 1e9);
		typeAndEnter(s);
		robot.waitForIdle();
	}

	@Override
	public void deltaColumn2(double m) {
		deltaColumn(m, deltaColumn2Location, 2);
	}
	
	@Override
	public void deltaColumn3(double m) {
		deltaColumn(m, deltaColumn3Location, 3);
	}
	
	private void deltaColumn(double m, Point deltaColumnLocation, int column) {
		String name = "Delta Column " + column;
		moveClickTypeAndEnter(deltaColumnLocation, name, "0.0001");
		final String s = format4f(m * 1e9);
		typeAndEnter(s);
		robot.waitForIdle();
	}

	@Override
	public void endRampColumn1(double m) {
		moveClickTypeAndEnter(endRampColumn1Location, "End Ramp Column 1", "0.0001");
		final String s = format4f(m * 1e9);
		typeAndEnter(s);
		robot.waitForIdle();
	}

	@Override
	public void loopCountColumn3(int count) {
		moveClickTypeAndEnter(loopCountColumn3Location, "Looop Count Column 3", Integer
				.toString(count));
		typeAndEnter(Integer.toString(count));
		robot.waitForIdle();
	}

	@Override
	public void startRampColumn1(double m) {
		moveClickTypeAndEnter(startRampColumn1Location, "Start Ramp Column 1", "0.0001");
		String s = format4f(m * 1e9);
		typeAndEnter(s);
		robot.waitForIdle();
	}

	@Override
	public void captureButton() {
		pressButton(captureButtonLocation, "Capture Button");
		robot.waitForIdle();
	}

	@Override
	public void startStripChart() {
		pressButton(startStripChartLocation, "Start Strip Chart");
		robot.waitForIdle();
	}
	
	@Override
	public void quitStripChart() {
		pressButton(quitStripChartLocation, "Quit Strip Chart");
		robot.waitForIdle();
	}

	@Override
	public void resetStripChart() {
		pressButton(resetStripChartLocation, "Reset Strip Chart");
		robot.waitForIdle();
	}

	@Override
	public void sleep(double timeS) {
    try {
      Sleeper.SYSTEM_SLEEPER.sleep((long) Math.round(timeS * 1000));
    } catch (InterruptedException e) {
      throw new MyRuntimeException(e.getMessage(), e);
    }
    /*
	  double millis = timeS * 1000;
	  long m = (long) Math.floor(millis);
	  double remainingNanos = (millis - m) * 1e6;
	  assert remainingNanos >= 0;
	  int n = (int) Math.min(remainingNanos, 999999);
		try {
			Sleeper.SYSTEM_SLEEPER.sleep(m, n);
		} catch (InterruptedException e) {
			throw new MyRuntimeException(e.getMessage(), e);
		}
		*/
	}

	@Override
	public void forceMode() {
		pressButton(forceModeLocation, "Force Mode");
		robot.waitForIdle();
		sleep(0.1);
	}

	@Override
	public void scriptMode() {
		pressButton(scriptModeLocation, "Script Mode");
		robot.waitForIdle();
		sleep(0.1);
	}

	@Override
	public void ping() {
	}

	@Override
	public void captureContinuous() {
		pressButton(realtimeLocation, "Realtime");
		pressButton(captureContinuousLocation, "Capture Continuous");
	}

	@Override
	public void abortCapture() {
		pressButton(abortCaptureLocation, "Abort Capture");
		robot.waitForIdle();
	}

	private static final Color pauseRedColour = new Color(255, 64, 64);
	private static final Color pauseOtherColour = new Color(212, 208, 200);
	
  @Override
  public boolean startOrPauseStripChartPixelIsRed() {
    Color color = robot.getPixelColor(startStripChartLocation.x, startStripChartLocation.y);
    if (color.equals(pauseOtherColour)) {
      return false;
    } else {
      if (color.equals(pauseRedColour)) {
        return true;
      } else {
        throw new IllegalStateException("Please move the Pause position a bit because: color=" + color);
      }
    }
  }

  @Override
  public void setupSingleScript(
    double startRampColumn1, double endRampColumn1, double velocityColumn1, double delayColumn1,
    double velocityColumn2, double deltaColumn2, double velocityColumn3, double deltaColumn3, int loopCountColumn3) {
    if (!Double.isNaN(startRampColumn1)) {
      startRampColumn1(startRampColumn1);
    }
    if (!Double.isNaN(endRampColumn1)) {
      endRampColumn1(endRampColumn1);
    }
    if (!Double.isNaN(velocityColumn1)) {
      velocityColumn1(velocityColumn1);
    }
    if (!Double.isNaN(delayColumn1)) {
      delayColumn1(delayColumn1);
    }
    if (!Double.isNaN(velocityColumn2)) {
      velocityColumn2(velocityColumn2);
    }
    if (!Double.isNaN(deltaColumn2)) {
      deltaColumn2(deltaColumn2);
    }
    if (!Double.isNaN(velocityColumn3)) {
      velocityColumn3(velocityColumn3);
    }
    if (!Double.isNaN(deltaColumn3)) {
      deltaColumn3(deltaColumn3);
    }
    if (loopCountColumn3 != -1) {
      loopCountColumn3(loopCountColumn3);
    }
  }
}
